package com.region.http.client.request.simple;

import com.region.common.adapter.json.JSONAdapterFactory;
import com.region.common.adapter.logging.LoggerAdapter;
import com.region.common.adapter.logging.LoggerAdapterFactory;
import com.region.common.utils.RandomUtils;
import com.region.common.utils.ReflectUtils;
import com.region.http.client.common.Constant;
import com.region.http.client.common.ContentType;
import com.region.http.client.common.RequestType;
import com.region.http.client.config.RequestAnnotationConfig;
import com.region.http.client.exception.ResponseErrorException;
import com.region.http.client.model.MultiFileTransferMeta;
import com.region.http.client.model.RequestParameter;
import com.region.http.client.model.ResponseData;
import com.region.http.client.request.AbstractTemplateHttpExecutor;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;

/**
 * @author 26225
 * @date 2024/9/7 16:06
 * @description
 */
public class SimpleHttpExecutor extends AbstractTemplateHttpExecutor {

    private final LoggerAdapter loggerAdapter = LoggerAdapterFactory.getLogger(getClass());

    @Override
    public ResponseData execute(String url, RequestAnnotationConfig requestAnnotationConfig, RequestParameter parameters) throws Throwable {
        HttpURLConnection connection = getUrlConnection(url, requestAnnotationConfig, parameters);
        configureParameter(connection, requestAnnotationConfig, parameters);
        try {
            // execute start
            connection.connect();
            int responseCode = connection.getResponseCode();
            if (responseCode == 200) {
                byte[] data = getResponseData(connection.getInputStream());
                ResponseData responseData = ResponseData.getInstance();
                setResponseHeader(responseData, connection);
                responseData.setBody(data);
                return responseData;
            } else {
                String msg = "The jdk http execute error, response code:" + responseCode;
                loggerAdapter.error(msg);
                throw new ResponseErrorException(msg);
            }
        } catch (Throwable throwable) {
            loggerAdapter.error("The request execute fail.", throwable);
            throw throwable;
        } finally {
            connection.disconnect();
        }
    }

    /**
     * get http url connection
     *
     * @param url
     * @return
     */
    private HttpURLConnection getUrlConnection(String url,
                                               RequestAnnotationConfig requestAnnotationConfig,
                                               RequestParameter parameters) {
        if (requestAnnotationConfig.getContentType() == ContentType.X_WWW_FORM_URLENCODED) {
            url = processUrl(parameters.getForm(), url);
        }
        if (url.startsWith(Constant.HTTP_PREFIX)) {
            try {
                URL httpURL = new URL(url);
                return (HttpURLConnection) httpURL.openConnection();
            } catch (Exception e) {
                loggerAdapter.error("The create Http URL connection fail.", e);
            }
        } else if (url.startsWith(Constant.HTTPS_PREFIX)) {
            try {
                HttpsURLConnection.setDefaultHostnameVerifier((urlHostName, session) -> true);
                SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
                sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                        // do nothing.
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                        // do nothing.
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }}, new java.security.SecureRandom());
                URL httpURL = new URL(url);
                HttpsURLConnection httpsURLConnection = (HttpsURLConnection) httpURL.openConnection();
                httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
                return httpsURLConnection;
            } catch (Exception e) {
                loggerAdapter.error("The create Https URL connection fail.", e);
            }
        }
        return null;
    }

    /**
     * configure request config and parameter
     * @param connection
     * @param requestAnnotationConfig
     * @param parameters
     */
    private void configureParameter(HttpURLConnection connection, RequestAnnotationConfig requestAnnotationConfig, RequestParameter parameters) {
        try {
            setRequestMethod(connection, requestAnnotationConfig.getRequestType());
            setHeaderValue(connection, requestAnnotationConfig.getContentType(), parameters.getHeaders());
            connection.setReadTimeout(requestAnnotationConfig.getReadTime());
            connection.setConnectTimeout(requestAnnotationConfig.getConnectTime());
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setUseCaches(false);
            connection.setDefaultUseCaches(false);
            if (requestAnnotationConfig.getContentType() == ContentType.MULTIPART_FORM_DATA_VALUE) {
                configMultipartFormData(connection, parameters);
            } else {
                // not exists files
                // Already configured parameter
                byte[] body = null;
                if (parameters.getBody() != null) {
                    if (parameters.getBody() instanceof String) {
                        body = String.valueOf(parameters.getBody()).getBytes(StandardCharsets.UTF_8);
                    } else {
                        body = String.valueOf(JSONAdapterFactory.getAdapter().toJSONString(parameters.getBody())).getBytes(StandardCharsets.UTF_8);
                    }
                }
                if (body != null) {
                    OutputStream outputStream = connection.getOutputStream();
                    outputStream.write(body);
                    outputStream.close();
                }
            }
        } catch (Exception e) {
            loggerAdapter.error("The configure config error.", e);
        }
    }

    private void setRequestMethod(HttpURLConnection httpURLConnection, RequestType requestType) throws ProtocolException {
        switch (requestType) {
            case POST:
                httpURLConnection.setRequestMethod("POST");
                break;
            case PUT:
                httpURLConnection.setRequestMethod("PUT");
                break;
            case DELETE:
                httpURLConnection.setRequestMethod("DELETE");
                break;
            case HEAD:
                httpURLConnection.setRequestMethod("HEAD");
                break;
            case TRACE:
                httpURLConnection.setRequestMethod("TRACE");
                break;
            case OPTIONS:
                httpURLConnection.setRequestMethod("OPTIONS");
                break;
            default:
                httpURLConnection.setRequestMethod("GET");
        }
    }

    private void setHeaderValue(HttpURLConnection connection, ContentType contentType, Map<String, Object> header) {
        if (contentType != null && contentType != ContentType.NULL) {
            connection.setRequestProperty("Content-Type", contentType.getValue());
        }
        if (header == null || header.isEmpty()) {
            return;
        }
        for (Map.Entry<String, Object> entry : header.entrySet()) {
            connection.setRequestProperty(entry.getKey(), String.valueOf(entry.getValue()));
        }
    }

    private byte[] getResponseData(InputStream inputStream) {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
            StringBuilder stringBuilder = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line);
            }
            return stringBuilder.toString().getBytes(StandardCharsets.UTF_8);
        } catch (Exception e) {
            loggerAdapter.error("The read input stream fail.", e);
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (Exception e) {
                loggerAdapter.error("The InputStream closed fail.", e);
            }
        }
        return null;
    }

    private void setResponseHeader(ResponseData responseData, HttpURLConnection connection) {
        Map<String, List<String>> headerFields = connection.getHeaderFields();
        for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
            String name = entry.getKey();
            if (name == null) {
                continue;
            }
            for (int i = 0; i < entry.getValue().size(); i++) {
                String value = entry.getValue().get(i);
                responseData.addHeader(name, value);
                String[] element = value.split(";");
                for (int j = 0; j < element.length; j++) {
                    String[] args = element[j].split("=");
                    if (args.length == 2) {
                        responseData.addNameValuePair(name, args[0].trim(), args[1].trim());
                    }
                    if (args.length == 1) {
                        responseData.addNameValuePair(name, args[0].trim(), null);
                    }
                }
            }
        }
    }

    /**
     * configure multipart data
     * @param connection
     * @param parameter
     * @throws IOException
     */
    private void configMultipartFormData(HttpURLConnection connection, RequestParameter parameter) throws IOException {
        String boundary = "629304877090867643445768";
        String end = "\r\n";
        String hyphens = "--";
        connection.setRequestProperty("Charset", "UTF-8");
        if (parameter.getForm() != null) {
            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
            Map<String, Object> form = parameter.getForm();
            for (Map.Entry<String, Object> entry : form.entrySet()) {
                String name = entry.getKey();
                Object value = entry.getValue();
                dataOutputStream.writeBytes(hyphens + boundary + end);
                if (value instanceof File) {
                    // file type
                    File file = (File) value;
                    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + file.getName() + "\"" + end);
                    dataOutputStream.writeBytes(end);
                    dataOutputStream.write(convertByteArray(new FileInputStream(file)));
                    dataOutputStream.writeBytes(end);
                } else if (value instanceof InputStream) {
                    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + RandomUtils.getId() + "\"" + end);
                    dataOutputStream.writeBytes(end);
                    dataOutputStream.write(convertByteArray((InputStream) value));
                    dataOutputStream.writeBytes(end);
                } else if (value instanceof byte[]) {
                    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + RandomUtils.getId() + "\"" + end);
                    dataOutputStream.writeBytes(end);
                    dataOutputStream.write((byte[]) value);
                    dataOutputStream.writeBytes(end);
                } else if (value instanceof MultiFileTransferMeta) {
                    MultiFileTransferMeta transferMeta = (MultiFileTransferMeta) value;
                    // process multipart file
                    if (transferMeta.getParameters() != null) {
                        for (Map.Entry<String, Object> data : transferMeta.getParameters().entrySet()) {
                            String fileName = data.getKey();
                            Object fileValue = data.getValue();
                            if (fileValue instanceof byte[]) {
                                dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + fileName + "\"" + end);
                                dataOutputStream.writeBytes(end);
                                dataOutputStream.write((byte[]) fileValue);
                                dataOutputStream.writeBytes(end);
                            }
                            if (fileValue instanceof InputStream) {
                                dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + fileName + "\"" + end);
                                dataOutputStream.writeBytes(end);
                                dataOutputStream.write(convertByteArray((InputStream) fileValue));
                                dataOutputStream.writeBytes(end);
                            }
                            if (fileValue instanceof File) {
                                File file = (File) fileValue;
                                dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"; filename=\"" + file.getName() + "\"" + end);
                                dataOutputStream.writeBytes(end);
                                dataOutputStream.write(convertByteArray(new FileInputStream(file)));
                                dataOutputStream.writeBytes(end);
                            }
                        }
                    }
                } else {
                    // generic type
                    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"" + end);
                    dataOutputStream.writeBytes(end);
                    dataOutputStream.writeBytes(String.valueOf(value));
                    dataOutputStream.writeBytes(end);
                }
            }
            dataOutputStream.writeBytes(hyphens + boundary + hyphens + end);
            dataOutputStream.flush();
        }
        if (parameter.getBody() != null) {
            connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
            Map<String, Object> data = ReflectUtils.getKeyValueByObject(parameter.getBody(), false);
            for (Map.Entry<String, Object> entry : data.entrySet()) {
                String name = entry.getKey();
                Object value = entry.getValue();
                dataOutputStream.writeBytes(hyphens + boundary + end);
                // generic type
                dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"" + end);
                dataOutputStream.writeBytes(end);
                dataOutputStream.write(String.valueOf(value).getBytes(StandardCharsets.UTF_8));
                dataOutputStream.writeBytes(end);
            }
            dataOutputStream.writeBytes(hyphens + boundary + hyphens + end);
            dataOutputStream.flush();
        }
    }

    /**
     * Check whether files exist in the request parameters
     * @param form
     * @param contentType
     * @return
     */
    private boolean existsFile(Map<String, Object> form, ContentType contentType) {
        if (contentType == ContentType.MULTIPART_FORM_DATA_VALUE) {
            for (Map.Entry<String, Object> entry : form.entrySet()) {
                Object value = entry.getValue();
                if (value instanceof File || value instanceof InputStream || value instanceof byte[] || value instanceof MultiFileTransferMeta) {
                    return true;
                }
            }
        }
        return false;
    }

    private byte[] convertByteArray(InputStream inputStream) {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();) {
            int len=0;
            byte[] bytes = new byte[64];
            while ((len = inputStream.read(bytes)) != -1) {
                outputStream.write(bytes);
            }
            return outputStream.toByteArray();
        } catch (Exception e) {
            loggerAdapter.error("The InputStream to convert byte fail.", e);
            return null;
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (Exception e) {
                loggerAdapter.error("The InputStream closed fail.", e);
            }
        }
    }

}
